#pragma once

#include <fmt/format.h>
#include <fmt/ostream.h>

#include <restinio/asio_include.hpp>

#include <condition_variable>
#include <future>
#include <mutex>
#include <optional>
#include <string>
#include <thread>

namespace restinio::tests
{

[[nodiscard]]
inline std::string
default_ip_addr()
{
	return "127.0.0.1";
}

template < typename LAMBDA >
void
do_with_socket(
	LAMBDA && lambda,
	const std::string & addr,
	std::uint16_t port )
{
	restinio::asio_ns::io_context io_context;
	restinio::asio_ns::ip::tcp::socket socket{ io_context };

	std::string str_addr{ addr };
	if( str_addr == "localhost" )
		str_addr = "127.0.0.1";
	else if( str_addr == "ip6-localhost" )
		str_addr = "::1";

	socket.connect(
		restinio::asio_ns::ip::tcp::endpoint{
			restinio::asio_ns::ip::make_address( str_addr ),
			port
		} );

	lambda( socket, io_context );
	socket.close();
}

inline std::string
do_request(
	const std::string & request,
	const std::string & addr,
	std::uint16_t port )
{
	std::string result;
	do_with_socket(
		[ & ]( auto & socket, auto & /*io_context*/ ){

			restinio::asio_ns::streambuf b;
			std::ostream req_stream(&b);
			req_stream << request;
			restinio::asio_ns::write( socket, b );

			std::ostringstream sout;
			restinio::asio_ns::streambuf response_stream;
			restinio::asio_ns::read_until( socket, response_stream, "\r\n\r\n" );

			sout << &response_stream;

			// Read until EOF, writing data to output as we go.
			restinio::asio_ns::error_code error;
			while( restinio::asio_ns::read(
					socket,
					response_stream,
					restinio::asio_ns::transfer_at_least(1),
					error) )
			{
				sout << &response_stream;
			}

			if ( !restinio::error_is_eof( error ) )
				throw std::runtime_error{
						fmt::format(
								RESTINIO_FMT_FORMAT_STRING( "read error: {}" ),
								restinio::fmtlib_tools::streamed( error ) ) };

			result = sout.str();
		},
		addr,
		port );

	return result;
}

template<typename Http_Server>
class other_work_thread_for_server_t
{
	Http_Server & m_server;

	std::thread m_thread;
public:
	other_work_thread_for_server_t(
		Http_Server & server )
		: m_server(server)
	{}
	~other_work_thread_for_server_t()
	{
		if( m_thread.joinable() )
			stop_and_join();
	}

	void
	run()
	{
		m_thread = std::thread( [this] {
			m_server.open_sync();
			m_server.io_context().run();
		} );

		// Ensure server was started:
		std::promise< void > p;
		restinio::asio_ns::post(
			m_server.io_context(),
			[&]{
				p.set_value();
			} );
		p.get_future().get();
	}

	void
	stop_and_join()
	{
		restinio::asio_ns::post(
			m_server.io_context(),
			[&]{
				m_server.close_sync();
			} );

		m_thread.join();
	}
};

class random_port_getter_t
{
	std::mutex m_lock;
	std::optional< std::uint16_t > m_value{};

	std::promise< std::uint16_t > m_port_promise;

public:
	[[nodiscard]]
	std::uint16_t
	port()
	{
		std::lock_guard< std::mutex > l{ m_lock };
		if( !m_value )
			m_value = m_port_promise.get_future().get();

		return *m_value;
	}

	[[nodiscard]]
	restinio::acceptor_post_bind_hook_t
	as_post_bind_hook()
	{
		return [this](asio_ns::ip::tcp::acceptor & acceptor) {
				m_port_promise.set_value(
						acceptor.local_endpoint().port() );
			};
	}
};

} /* namespace restinio::tests */

